/*
 * @(#)UsfDispatcher.java $version 2010. 8. 11
 *
 * Copyright 2007-2010 NHN Corp. All rights Reserved. 
 * NHN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.nhncorp.usf.core;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import javax.script.ScriptException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import com.nhncorp.lucy.common.util.SpringObjectFactory;
import com.nhncorp.lucy.web.helper.ServletHelper;
import com.nhncorp.usf.core.config.AttributeInfo;
import com.nhncorp.usf.core.config.AttributeInfo.ATTRIBUTE_NAME;
import com.nhncorp.usf.core.config.runtime.Action;
import com.nhncorp.usf.core.config.runtime.BusinessObject;
import com.nhncorp.usf.core.config.runtime.PageInfo;
import com.nhncorp.usf.core.config.runtime.ResultPageInfo;
import com.nhncorp.usf.core.config.runtime.ResultPageInfos;
import com.nhncorp.usf.core.config.runtime.ScriptEngineGlobals;
import com.nhncorp.usf.core.exception.DefaultExceptionHandler;
import com.nhncorp.usf.core.exception.ExceptionHandler;
import com.nhncorp.usf.core.parser.ConfigBuilder;
import com.nhncorp.usf.core.parser.ConfigDOMBuilder;
import com.nhncorp.usf.core.parser.ParseConfigurationException;
import com.nhncorp.usf.core.result.ResultGenerator;
import com.nhncorp.usf.core.result.helper.FreemarkerConfigFactory;
import com.nhncorp.usf.core.service.ServiceExecutor;
import com.nhncorp.usf.core.service.UsfServiceExecutor;
import com.nhncorp.usf.core.servlet.ServletContextHolder;
import com.nhncorp.usf.core.util.MapUtil;
import com.nhncorp.usf.core.util.ScriptUtil;
import com.nhncorp.usf.core.util.StringUtil;
import com.nhncorp.usf.core.xwork.CommonUsfActionInvocation;
import com.nhncorp.usf.core.xwork.DefaultUsfActionInvocation;
import com.nhncorp.usf.core.xwork.UsfActionInvocation;

/**
 * Main for processing usf
 * 
 * @author Web Platform Development Team
 */
public final class UsfDispatcher {
    Log log = LogFactory.getLog(UsfDispatcher.class);

    public static final String REQUEST_PARAM_NAME = "requestParam";
    public static final String DEFAULT_ACTION_PARAMETER = "action";
    public static final String PARAM_PREFIX = "p";

    private boolean devMode;

    /**
     * requestUri 가 parameter 를 포함하는 형식 인지 여부
     * */
    private boolean parameterUri;

    /**
     * root path of web application
     */
    private String rootPath = ServletContextHolder.get().getRealPath("");

    /**
     * action parameter
     */
    private String actionParameter;

    /**
     * Map to contain {@link PageInfo} {@link Map}.
     */
    private Map<String, PageInfo> pages = new ConcurrentHashMap<String, PageInfo>();

    /**
     * USF Operation Configuration.
     */
    private UsfConfiguration usfConfiguration;

    /**
     * Object to build Dynamic Configuration.
     */
    private ConfigBuilder configBuilder;

    /**
     * object to execute business using {@link BusinessObject#getMethodInfos()}.
     */
    private ServiceExecutor serviceExecutor;

    /**
     * Object to generate Result.
     */
    private ResultGenerator resultGenerator;

    /**
     * Holder to return {@link UsfDispatcher}
     */
    private static final class UsfDispatcherHolder {
        static final UsfDispatcher USF_DISPATCHER;

        static {
            USF_DISPATCHER = new UsfDispatcher();
        }
    }

    /**
     * Base Construction.
     */
    private UsfDispatcher() {
    }

    /**
     * return {@link UsfDispatcher}.
     * 
     * @return the UsfDispatcher instance
     */
    public static UsfDispatcher getInstance() {
        return UsfDispatcherHolder.USF_DISPATCHER;
    }

    /**
     * initialize USF.
     * 
     * @param usfConfiguration
     *            the UsfConfiguration
     * @return boolean the initialize success
     * @throws ServletException
     *             the ServletException
     * @throws IOException
     *             the IOException
     */
    @SuppressWarnings("unchecked")
    public boolean initialize(UsfConfiguration usfConfiguration)
            throws ServletException, IOException {
        this.usfConfiguration = usfConfiguration;
        AttributeInfo attributeInfo = AttributeInfo.getInstance();

        actionParameter = attributeInfo
                .getAttribute(ATTRIBUTE_NAME.actionParameter);

        if (StringUtil.isEmpty(actionParameter)) {
            actionParameter = DEFAULT_ACTION_PARAMETER;
        }

        String templateDirPath = ServletContextHolder.get().getRealPath(
                usfConfiguration.getTemplates());

        // devMode
        String devModeString = attributeInfo
                .getAttribute(ATTRIBUTE_NAME.devMode);
        boolean devMode = StringUtil.isNotEmpty(devModeString)
                && Boolean.parseBoolean(devModeString);

        if (devMode) {
            FreemarkerConfigFactory.setDevMode(devMode);
        } else {
            String templateUpdateDelay = attributeInfo
                    .getAttribute(ATTRIBUTE_NAME.templateUpdateDelay);

            if (StringUtil.isNotEmpty(templateUpdateDelay)) {
                FreemarkerConfigFactory.setTemplateUpdateDelay(Integer
                        .valueOf(templateUpdateDelay));
            }

        }

        this.devMode = devMode;

        String freemarkerLoggerName = attributeInfo
                .getAttribute(ATTRIBUTE_NAME.freemarkerLogger);

        if (freemarkerLoggerName != null) {
            try {
                FreemarkerConfigFactory
                        .setFreemarkerLogger(freemarkerLoggerName);
            } catch (ClassNotFoundException e) {
                log.debug("fail to setting freemarker Logger : "
                        + e.getMessage());
            }
        }

        String freemarkerExceptionHandler = attributeInfo
                .getAttribute(ATTRIBUTE_NAME.freemarkerExceptionHandler);

        if (freemarkerExceptionHandler != null) {
            try {
                FreemarkerConfigFactory
                        .setTemplateExceptionHandler(freemarkerExceptionHandler);
            } catch (Exception e) {
                log.debug("fail to setting freemarker exception handler : "
                        + e.getMessage());
            }
        }

        String freemarkerOutputEncoding = attributeInfo
                .getAttribute(ATTRIBUTE_NAME.freemarkerOutputEncoding);

        if (freemarkerOutputEncoding != null) {
            try {
                FreemarkerConfigFactory
                        .setOutputEncoding(freemarkerOutputEncoding);
            } catch (Exception e) {
                log.debug("Sets the freemarker output_encoding : "
                        + freemarkerOutputEncoding);
            }
        }

        String freemarkerURLEscapingCharset = attributeInfo
                .getAttribute(ATTRIBUTE_NAME.freemarkerURLEscapingCharset);

        if (freemarkerURLEscapingCharset != null) {
            try {
                FreemarkerConfigFactory
                        .setURLEscapingCharset(freemarkerURLEscapingCharset);
            } catch (Exception e) {
                log.debug("Sets the freemarker url_escaping_charset : "
                        + freemarkerURLEscapingCharset);
            }
        }

        // configBuilder
        String runtimeConfigBuilder = attributeInfo
                .getAttribute(ATTRIBUTE_NAME.runtimeConfigBuilder);

        if (StringUtil.isEmpty(runtimeConfigBuilder, true)) {
            runtimeConfigBuilder = ConfigDOMBuilder.class.getName();
        }

        configBuilder = SpringObjectFactory.getInstance().getObject(
                runtimeConfigBuilder);

        // ExceptionHandler
        String exceptionHandlerString = attributeInfo
                .getAttribute(AttributeInfo.ATTRIBUTE_NAME.exceptionHandler
                        .name());

        if (StringUtil.isEmpty(exceptionHandlerString, true)) {
            exceptionHandlerString = DefaultExceptionHandler.class.getName();
        }

        // service executor
        String serviceExecutorString = attributeInfo
                .getAttribute(AttributeInfo.ATTRIBUTE_NAME.serviceExecutor
                        .name());

        if (StringUtil.isEmpty(serviceExecutorString, true)) {
            serviceExecutorString = UsfServiceExecutor.class.getName();
        }

        serviceExecutor = SpringObjectFactory.getInstance().getObject(
                serviceExecutorString);

        // Result Generator
        String resultGeneratorString = attributeInfo
                .getAttribute(AttributeInfo.ATTRIBUTE_NAME.resultGenerator
                        .name());

        if (StringUtil.isEmpty(resultGeneratorString, true)) {
            resultGeneratorString = ResultGenerator.class.getName();
        }

        resultGenerator = SpringObjectFactory.getInstance().getObject(
                resultGeneratorString);

        // loading all dynamic configurations and make PageInfo about each
        // dynamic configuration
        IOFileFilter fileFilter = FileFilterUtils
                .suffixFileFilter(Constants.RUNTIME_XML);
        IOFileFilter dirFilter = FileFilterUtils.trueFileFilter();
        log.debug("templateDirPath : " + templateDirPath);
        Collection configFiles = FileUtils.listFiles(new File(templateDirPath),
                fileFilter, dirFilter);
        Iterator<File> iterator = configFiles.iterator();
        ExceptionHandler exceptionHandler = SpringObjectFactory.getInstance()
                .getObject(exceptionHandlerString);
        File file = null;
        try {
            while (iterator.hasNext()) {
                file = iterator.next();
                String dynamicConfig = StringUtil.substringAfter(
                        file.getPath(), rootPath);
                dynamicConfig = FilenameUtils.separatorsToUnix(dynamicConfig);
                String validatorFile = dynamicConfig.replaceAll("_run.xml",
                        "_validator.xml");

                PageInfo pageInfo = configBuilder
                        .build(ServletContextHolder.get().getResourceAsStream(
                                dynamicConfig), ServletContextHolder.get()
                                .getResourceAsStream(validatorFile),
                                usfConfiguration.getTemplates());

                if (pageInfo != null) {
                    pages.put(dynamicConfig, pageInfo);
                }
            }
        } catch (Exception except) {
            if (file != null) {
                log.error("Error configuration file :" + file.getName());
            }

            Exception exception = new ParseConfigurationException(except);
            log.error(exception);
            throw new ServletException(exception);
        }
        // loading jar dynamic configurations and make PageInfo about each
        // dynamic configuration
        ResourcePatternResolver resourceLoader = new PathMatchingResourcePatternResolver();
        Resource[] resources = resourceLoader.getResources("classpath*:"
                + FilenameUtils.separatorsToUnix(usfConfiguration
                        .getTemplates().substring(1)) + "/**/*"
                + Constants.RUNTIME_XML);
        Resource resource = null;

        try {
            for (int i = 0; i < resources.length; i++) {
                resource = resources[i];
                String dynamicConfig = resource.getURI().toString();
                dynamicConfig = dynamicConfig.substring(dynamicConfig
                        .indexOf(FilenameUtils
                                .separatorsToUnix(usfConfiguration
                                        .getTemplates())));
                String validatorFile = resource.getURL().toString()
                        .replaceAll("_run.xml", "_validator.xml");
                InputStream validatorFileStream = null;
                try {

                    validatorFileStream = new URL(validatorFile).openStream();
                } catch (IOException e) {
                    log.debug(resource.getFilename()
                            + " do not have a validator file.");
                }

                PageInfo pageInfo = configBuilder.build(resource.getURL()
                        .openStream(), validatorFileStream, usfConfiguration
                        .getTemplates());

                if (pageInfo != null) {
                    pages.put(dynamicConfig, pageInfo);
                }
            }
        } catch (Exception except) {
            if (resource != null) {
                log.error("Error configuration file :" + resource.getFilename());
            }

            Exception exception = new ParseConfigurationException(except);
            log.error(exception);
            throw new ServletException(exception);
        }

        // invoker & businessExecutor
        try {
            serviceExecutor.setExceptionHandler(exceptionHandler);
            loadCustomScripts(usfConfiguration);
        } catch (Exception except) {
            except.printStackTrace();
            log.error(except);
            throw new ServletException(except);
        }

        // urlType
        String parameterUriString = attributeInfo
                .getAttribute(ATTRIBUTE_NAME.parameterUri);
        parameterUri = StringUtil.isNotEmpty(parameterUriString)
                && Boolean.parseBoolean(parameterUriString);

        return true;
    }

    /**
     * Load custom scripts.
     * 
     * @param usfConfiguration
     *            the usf configuration
     * @throws ScriptException
     *             the script exception
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    private void loadCustomScripts(UsfConfiguration usfConfiguration)
            throws ScriptException, IOException {
        String scriptPath = usfConfiguration.getScripts();

        if (scriptPath != null) {
            String scriptDirPath = ServletContextHolder.get().getRealPath(
                    scriptPath);
            ServletContextHolder.setContextRealPath(scriptDirPath);
            ScriptUtil.loadJSLib(scriptDirPath);
            String scriptPropertyFile = usfConfiguration.getScriptProperty();

            if (scriptPropertyFile == null) {
                ScriptUtil.loadJSFile(scriptDirPath);
            } else {
                ScriptUtil.loadProperty(scriptDirPath, scriptPropertyFile);
            }
        }
    }

    /**
     * exeucte action.
     * 
     * @param actionUri
     *            the action uri
     * @throws Exception
     *             the Exception
     */
    public void processingAction(String actionUri) throws Exception {
        // return PageInfo about ActionUri
        PageInfo pageInfo = getPageInfo(actionUri);

        // convert parameterMap to DataMap
        Map<String, Object> dataStructure = getRequestData(ServletHelper
                .getRequest());

        Action actionInfo = getActionInfo(pageInfo, dataStructure);
        try {
            Action commonActionInfo = pageInfo.getCommonAction();

            if (commonActionInfo != null) {
                CommonUsfActionInvocation commonActionInvocation = new CommonUsfActionInvocation(
                        serviceExecutor, new DummyResultGenerator(), pageInfo,
                        commonActionInfo, dataStructure);
                commonActionInvocation.invoke();
                Map<String, Object> commonActionDataMap = commonActionInvocation
                        .getDataMap();
                dataStructure.putAll(commonActionDataMap);
            }

            // execute action with parameterMap
            DefaultUsfActionInvocation actionInvocation = new DefaultUsfActionInvocation(
                    serviceExecutor, resultGenerator, pageInfo, actionInfo,
                    dataStructure);
            actionInvocation.invoke();
            actionInvocation.clear();
            actionInvocation = null;
        } catch (Exception except) {
            except.printStackTrace();
            String errorMessage = "fail in execute action '"
                    + actionInfo.getId() + "'";
            throw new Exception(errorMessage, except);
        }
    }

    /**
     * {@code request} 에서 Parameter Data 를 추출한다.
     * 
     * @param request
     *            {@link HttpServletRequest}
     * @return 추출한 Paramenter Data Map
     * */
    @SuppressWarnings("unchecked")
    public Map<String, Object> getRequestData(HttpServletRequest request) {
        Map<String, Object> dataStructure = new HashMap<String, Object>();

        if (parameterUri) {
            String uri = request.getRequestURI();
            int idx = uri.indexOf("." + usfConfiguration.getSuffix());

            if (uri.length() > idx + usfConfiguration.getSuffix().length() + 2) {
                uri = uri.substring(idx + usfConfiguration.getSuffix().length()
                        + 2);
                String[] arr = uri.split("/");

                if (arr != null) {
                    for (int i = 0; i < arr.length; i++) {
                        dataStructure.put(PARAM_PREFIX + String.valueOf(i + 1),
                                arr[i]);
                    }
                }
            }
        }

        Map<String, Object> requestParameterMap = request.getParameterMap();

        for (Entry<String, Object> entry : requestParameterMap.entrySet()) {
            String[] value = (String[]) entry.getValue();

            if (value != null && value.length == 1) {
                dataStructure.put(entry.getKey(), value[0]);
            } else {
                dataStructure.put(entry.getKey(), value);
            }
        }

        Map requestParam = new HashMap<String, Object>();
        requestParam.putAll(dataStructure);
        dataStructure.put(REQUEST_PARAM_NAME, requestParam);
        return dataStructure;
    }

    /**
     * destroy USF.
     */
    public void destroy() {
        ScriptEngineGlobals.clear();
        pages.clear();
    }

    // -- private
    // ----------------------------------------------------------------------------------------------------------------

    /**
     * return {@link PageInfo}.
     * 
     * @param dynamicConfig
     *            the dynamic configuration file name
     * @return the PageInfo
     */
    public PageInfo getPageInfo(String dynamicConfig) {
        PageInfo pageInfo = pages.get(dynamicConfig);

        if (devMode) {
            try {
                log.debug("checking dynamic configuration");

                String filename = ServletContextHolder.get().getRealPath(
                        dynamicConfig);
                File tempfile = new File(filename);
                long pageCreatedTime = (pageInfo == null) ? -1 : pageInfo
                        .getCreatedTime();

                if (FileUtils.isFileNewer(tempfile, pageCreatedTime)) {
                    pageInfo = configBuilder.build(ServletContextHolder.get()
                            .getResourceAsStream(dynamicConfig),
                            usfConfiguration.getTemplates());
                    pages.put(dynamicConfig, pageInfo);
                    log.debug("dynamic configuration is reloaded.");
                }
            } catch (Exception except) {
                log.error("failure in reloading", except);
            }
        }

        if (pageInfo == null) {
            throw new IllegalArgumentException("'" + dynamicConfig
                    + "' does not exist.");
        }

        return pageInfo;
    }

    /**
     * extract {@link Action} using dataMap provided by client user. If
     * ActionInfo to be executed does not defined, method returns default
     * AciontInfo named {@code Constants.DEFAULT_ACTION_ID}.
     * 
     * @param pageInfo
     *            the PageInfo
     * @param dataStructure
     *            the data map
     * @return the Action
     */
    public Action getActionInfo(PageInfo pageInfo,
            Map<String, Object> dataStructure) {
        Action actionInfo = pageInfo.getActionInfo(actionParameter
                + Constants.ACTION_METHOD_SUFFRIX);
        log.debug("ap : " + actionParameter);
        String actionParameterValue = MapUtil.getActionString(dataStructure,
                actionParameter);

        if (actionInfo == null) {
            actionInfo = pageInfo.getActionInfo(actionParameterValue);
        }

        if (actionInfo == null) {
            actionInfo = pageInfo.getActionInfo(pageInfo.getDefaultActionId());
        }

        if (actionInfo == null) {
            throw new IllegalArgumentException("Action is not exist!!");
        }

        return actionInfo;
    }

    /**
     * define validation of action.
     * 
     * @param request
     *            the HttpServletRequest
     * @return boolean the valid
     */
    public boolean isValidAction(HttpServletRequest request) {
        if (!parameterUri) {
            if (!request.getRequestURI().endsWith(
                    "." + usfConfiguration.getSuffix())) {
                return false;
            }
        } else {
            if (!request.getRequestURI().contains(
                    "." + usfConfiguration.getSuffix())) {
                return false;
            }
        }

        String actionUri = getActionUri(request);

        if (devMode) {
            try {
                log.debug("actionUri : " + actionUri);

                if (ServletContextHolder.get().getResource(actionUri) != null) {
                    return true;
                }

            } catch (Exception except) {
                log.debug(except);
            }
        }

        return pages.containsKey(actionUri);
    }

    /**
     * extract ActionUri using URI.
     * 
     * @param request
     *            the HttpServletRequest
     * @return uri the action uri
     */
    public String getActionUri(HttpServletRequest request) {
        String requestUri = request.getRequestURI();
        String uri = requestUri.replaceFirst(request.getContextPath(), "");

        String dotSuffix = "." + usfConfiguration.getSuffix();

        if (parameterUri) {
            uri = uri.substring(0, uri.indexOf(dotSuffix));
        }

        log.debug("Context path : " + request.getContextPath());
        log.debug("Request URI : " + requestUri);
        log.debug("URI : " + uri);

        String dynamicConfig = usfConfiguration.getTemplates()
                + uri.replace(dotSuffix, "") + Constants.RUNTIME_XML;
        return FilenameUtils.separatorsToUnix(dynamicConfig);
    }

    /**
     * whether devmode is true or not.
     * 
     * @return the dev mode value
     */
    public boolean isDevMode() {
        return this.devMode;
    }

    /**
     * Delegating method to return the root directory of web.
     * 
     * @return the root path
     */
    public String getRootPath() {
        return this.rootPath;
    }

    /**
     * Gets the action parameter.
     * 
     * @return the action parameter
     */
    public String getActionParameter() {
        return actionParameter;
    }

    public ServiceExecutor getServiceExecutor() {
        return serviceExecutor;
    }

    public ResultGenerator getResultGenerator() {
        return resultGenerator;
    }

    /**
     * @return 파싱한 run.xml 페이지 정보
     * */
    public Map<String, PageInfo> getPages() {
        return pages;
    }

    /**
     * 기능이 없는 Dummy Class
     * 
     * @author Web Platform Development Team
     * @version $Rev: 10957 $, $Date: 2010-08-11 16:28:15 +0900 (수, 11 8 2010) $
     */
    class DummyResultGenerator extends ResultGenerator {
        public DummyResultGenerator() {
        }

        @Override
        public void executeResult(UsfActionInvocation usfActionInvocation)
                throws Exception {
            return;
        }

        @Override
        public ResultPageInfo getResultPageInfo(
                ResultPageInfos globalResultPageInfos,
                ResultPageInfos localResultPageInfos,
                Map<String, Object> requestDataMap) throws Exception {
            return null;
        }

    }
}
